/*
 * Copyright 2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.api.internal.project.taskfactory

import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.internal.AbstractTask
import org.gradle.api.internal.TaskInternal
import org.gradle.api.internal.file.FileCollectionFactory
import org.gradle.api.internal.file.FileResolver
import org.gradle.api.internal.tasks.properties.DefaultPropertyWalker
import org.gradle.api.internal.tasks.properties.DefaultTaskProperties
import org.gradle.api.internal.tasks.properties.DefaultTypeMetadataStore
import org.gradle.api.internal.tasks.properties.ModifierAnnotationCategory
import org.gradle.api.internal.tasks.properties.annotations.PropertyAnnotationHandler
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskPropertyTestUtils
import org.gradle.api.tasks.incremental.IncrementalTaskInputs
import org.gradle.cache.internal.TestCrossBuildInMemoryCacheFactory
import org.gradle.internal.execution.WorkValidationException
import org.gradle.internal.reflect.DirectInstantiator
import org.gradle.internal.reflect.JavaReflectionUtil
import org.gradle.internal.reflect.annotations.impl.DefaultTypeAnnotationMetadataStore
import org.gradle.internal.service.ServiceRegistryBuilder
import org.gradle.internal.service.scopes.ExecutionGlobalServices
import org.gradle.test.fixtures.AbstractProjectBuilderSpec
import org.gradle.test.fixtures.file.TestFile
import org.gradle.work.InputChanges
import spock.lang.Unroll

import java.util.concurrent.Callable

import static org.apache.commons.io.FileUtils.touch
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.Bean
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.Bean2
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.BrokenTaskWithInputDir
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.BrokenTaskWithInputFiles
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.NamedBean
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskOverridingDeprecatedIncrementalChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskOverridingInputChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskUsingInputChanges
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithBooleanInput
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithBridgeMethod
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithDestroyable
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithIncrementalAction
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithInheritedMethod
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithInput
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithInputDir
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithInputFile
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithInputFiles
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithJavaBeanCornerCaseProperties
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithLocalState
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMixedMultipleIncrementalActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMultiParamAction
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMultipleIncrementalActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMultipleInputChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMultipleMethods
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithMultipleProperties
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithNestedBean
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithNestedBeanWithPrivateClass
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithNestedIterable
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithNestedObject
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalInputFile
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalNestedBean
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalNestedBeanWithPrivateType
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalOutputDir
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalOutputDirs
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalOutputFile
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOptionalOutputFiles
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOutputDir
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOutputDirs
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOutputFile
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOutputFiles
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverloadedActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverloadedDeprecatedIncrementalAndInputChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverloadedIncrementalAndInputChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverloadedInputChangesActions
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverriddenIncrementalAction
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverriddenInputChangesAction
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithOverriddenMethod
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithProtectedMethod
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithSingleParamAction
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TaskWithStaticMethod
import static org.gradle.api.internal.project.taskfactory.AnnotationProcessingTasks.TestTask
import static org.gradle.internal.service.scopes.ExecutionGlobalServices.IGNORED_METHOD_ANNOTATIONS
import static org.gradle.internal.service.scopes.ExecutionGlobalServices.PROPERTY_TYPE_ANNOTATIONS

class AnnotationProcessingTaskFactoryTest extends AbstractProjectBuilderSpec {
    private AnnotationProcessingTaskFactory factory
    private ITaskFactory delegate
    def services = ServiceRegistryBuilder.builder().provider(new ExecutionGlobalServices()).build()
    def taskClassInfoStore = new DefaultTaskClassInfoStore(new TestCrossBuildInMemoryCacheFactory())
    def cacheFactory = new TestCrossBuildInMemoryCacheFactory()
    def typeAnnotationMetadataStore = new DefaultTypeAnnotationMetadataStore(
        [],
        ModifierAnnotationCategory.asMap(PROPERTY_TYPE_ANNOTATIONS),
        ["java", "groovy"],
        [],
        [Object, GroovyObject],
        [ConfigurableFileCollection, Property],
        IGNORED_METHOD_ANNOTATIONS,
        { false },
        cacheFactory
    )
    def propertyWalker = new DefaultPropertyWalker(new DefaultTypeMetadataStore([], services.getAll(PropertyAnnotationHandler), [Optional, SkipWhenEmpty], typeAnnotationMetadataStore, cacheFactory))

    @SuppressWarnings("GroovyUnusedDeclaration")
    private String inputValue = "value"
    private File testDir
    private File existingFile
    private File missingFile
    private TestFile existingDir
    private File missingDir
    private File missingDir2

    def setup() {
        delegate = Mock(ITaskFactory)
        factory = new AnnotationProcessingTaskFactory(DirectInstantiator.INSTANCE, taskClassInfoStore, delegate)
        testDir = temporaryFolder.testDirectory
        existingFile = testDir.file("file.txt").touch()
        missingFile = testDir.file("missing.txt")
        existingDir = testDir.file("dir").createDir()
        missingDir = testDir.file("missing-dir")
        missingDir2 = testDir.file("missing-dir2")
    }

    FileResolver getFileResolver() {
        return project.fileResolver
    }

    FileCollectionFactory getFileCollectionFactory() {
        return project.services.get(FileCollectionFactory)
    }

    def doesNothingToTaskWithNoTaskActionAnnotations() {
        given:
        def task = expectTaskCreated(DefaultTask)

        expect:
        task.getActions().isEmpty()
    }

    def propagatesExceptionThrownByTaskActionMethod() {
        given:
        def action = Stub(Runnable, {
            run() >> {
                throw new RuntimeException()
            }
        })
        def task = expectTaskCreated(TestTask, action)

        when:
        task.getActions().get(0).execute(task)

        then:
        thrown(RuntimeException)
    }

    @SuppressWarnings("GrDeprecatedAPIUsage")
    def createsContextualActionForIncrementalTaskAction() {
        given:
        def action = Mock(Action)
        def task = expectTaskCreated(TaskWithIncrementalAction, action)

        when:
        execute(task)

        then:
        1 * action.execute(_ as IncrementalTaskInputs)
        0 * _
    }

    def createsContextualActionForInputChangesTaskAction() {
        given:
        def action = Mock(Action)
        def task = expectTaskCreated(TaskUsingInputChanges, action)

        when:
        execute(task)

        then:
        1 * action.execute(_ as InputChanges)
        0 * _
    }

    @SuppressWarnings("GrDeprecatedAPIUsage")
    def createsContextualActionForOverriddenIncrementalTaskAction() {
        given:
        def action = Mock(Action)
        def superAction = Mock(Action)
        def task = expectTaskCreated(TaskWithOverriddenIncrementalAction, action, superAction)

        when:
        execute(task)

        then:
        1 * action.execute(_ as IncrementalTaskInputs)
        0 * _
    }

    @SuppressWarnings("GrDeprecatedAPIUsage")
    def "uses IncrementalTaskInputsMethod when it is overriden"() {
        given:
        def changesAction = Mock(Action)
        def task = expectTaskCreated(TaskOverridingDeprecatedIncrementalChangesActions, changesAction)

        when:
        execute(task)

        then:
        1 * changesAction.execute(_ as InputChanges)
        1 * changesAction.execute(_ as IncrementalTaskInputs)
        0 * _
    }

    def "uses InputChanges method when two actions are present on the same class"() {
        given:
        def changesAction = Mock(Action)
        def task = expectTaskCreated(TaskWithOverloadedDeprecatedIncrementalAndInputChangesActions, changesAction)

        when:
        execute(task)

        then:
        1 * changesAction.execute(_ as InputChanges)
        0 * _
    }

    def "uses overridden InputChanges when two actions are present on the base class"() {
        given:
        def changesAction = Mock(Action)
        def task = expectTaskCreated(TaskOverridingInputChangesActions, changesAction)

        when:
        execute(task)

        then:
        1 * changesAction.execute(_ as InputChanges)
        0 * _
    }

    def createsContextualActionForOverriddenInputChangesTaskAction() {
        given:
        def action = Mock(Action)
        def superAction = Mock(Action)
        def task = expectTaskCreated(TaskWithOverriddenInputChangesAction, action, superAction)

        when:
        execute(task)

        then:
        1 * action.execute(_ as InputChanges)
        0 * _
    }

    def cachesClassMetaInfo() {
        given:
        def taskInfo1 = taskClassInfoStore.getTaskClassInfo(TaskWithInputFile)
        def taskInfo2 = taskClassInfoStore.getTaskClassInfo(TaskWithInputFile)

        expect:
        taskInfo1.is(taskInfo2)
    }

    @Unroll
    def "fails for #type.simpleName"() {
        when:
        expectTaskCreated(type)

        then:
        def e = thrown GradleException
        e.message == failureMessage

        where:
        type                                                | failureMessage
        TaskWithMultipleIncrementalActions                  | "Cannot have multiple @TaskAction methods accepting an InputChanges or IncrementalTaskInputs parameter."
        TaskWithStaticMethod                                | "Cannot use @TaskAction annotation on static method TaskWithStaticMethod.doStuff()."
        TaskWithMultiParamAction                            | "Cannot use @TaskAction annotation on method TaskWithMultiParamAction.doStuff() as this method takes multiple parameters."
        TaskWithSingleParamAction                           | "Cannot use @TaskAction annotation on method TaskWithSingleParamAction.doStuff() because int is not a valid parameter to an action method."
        TaskWithOverloadedActions                           | "Cannot use @TaskAction annotation on multiple overloads of method TaskWithOverloadedActions.doStuff()"
        TaskWithOverloadedInputChangesActions               | "Cannot use @TaskAction annotation on multiple overloads of method TaskWithOverloadedInputChangesActions.doStuff()"
        TaskWithOverloadedIncrementalAndInputChangesActions | "Cannot use @TaskAction annotation on multiple overloads of method TaskWithOverloadedIncrementalAndInputChangesActions.doStuff()"
        TaskWithMultipleInputChangesActions                 | "Cannot have multiple @TaskAction methods accepting an InputChanges or IncrementalTaskInputs parameter."
        TaskWithMixedMultipleIncrementalActions             | "Cannot have multiple @TaskAction methods accepting an InputChanges or IncrementalTaskInputs parameter."
    }

    @Unroll
    def "works for #type.simpleName"() {
        given:
        def action = Mock(Runnable)
        def task = expectTaskCreated(type, action)

        when:
        execute(task)

        then:
        times * action.run()

        where:
        type                     | times
        TestTask                 | 1
        TaskWithInheritedMethod  | 1
        TaskWithOverriddenMethod | 1
        TaskWithProtectedMethod  | 1
        TaskWithMultipleMethods  | 3
    }

    @Unroll
    def "validation succeeds when #property #value on #type.simpleName"() {
        given:
        def task = expectTaskCreated(type, this[value])

        expect:
        execute(task)

        where:
        type                | property       | value
        TaskWithInputFile   | 'input-file'   | 'existingFile'
        TaskWithOutputFile  | 'output-file'  | 'existingFile'
        TaskWithOutputDir   | 'output-dir'   | 'existingDir'
        TaskWithInputDir    | 'input-dir'    | 'existingDir'
        TaskWithInput       | 'input'        | 'inputValue'
    }

    @Unroll
    def "validation succeeds when list #property contains #value on #type.simpleName"() {
        given:
        def task = expectTaskCreated(type, [this[value]] as List)

        expect:
        execute(task)

        where:
        type                | property       | value
        TaskWithInputFiles  | 'input-files'  | 'testDir'
        TaskWithOutputFiles | 'output-files' | 'existingFile'
        TaskWithOutputDirs  | 'output-dirs'  | 'existingDir'
    }

    @Unroll
    def "validation succeeds when optional #property is omitted on #type.simpleName"() {
        given:
        def task = expectTaskCreated(type, arguments as Object[])

        expect:
        execute(task)

        where:
        type                                      | property
        TaskWithOptionalInputFile                 | 'input-file'
        TaskWithOptionalOutputFile                | 'output-file'
        TaskWithOptionalOutputFiles               | 'output-files'
        TaskWithOptionalOutputDir                 | 'output-dir'
        TaskWithOptionalOutputDirs                | 'output-dirs'
        TaskWithOptionalNestedBean                | 'bean'
        TaskWithOptionalNestedBeanWithPrivateType | 'private-bean'
        arguments = type == TaskWithOptionalNestedBean ? [null] : []
    }

    def validationActionSucceedsWhenSpecifiedOutputFileDoesNotExist() {
        given:
        def task = expectTaskCreated(TaskWithOutputFile, new File(testDir, "subdir/output.txt"))

        when:
        execute(task)

        then:
        new File(testDir, "subdir").isDirectory()
    }

    def validationActionSucceedsWhenSpecifiedOutputFilesDoesNotExist() {
        given:
        def task = expectTaskCreated(TaskWithOutputFiles, [new File(testDir, "subdir/output.txt"), new File(testDir, "subdir2/output.txt")] as List)

        when:
        execute(task)

        then:
        new File(testDir, "subdir").isDirectory()
        new File(testDir, "subdir2").isDirectory()
    }

    def validationActionSucceedsWhenSpecifiedOutputDirectoryDoesNotExist() {
        given:
        def task = expectTaskCreated(TaskWithOutputDir, missingDir)

        when:
        execute(task)

        then:
        task.outputDir.isDirectory()
    }

    def validationActionSucceedsWhenSpecifiedOutputDirectoriesDoesNotExist() {
        given:
        def task = expectTaskCreated(TaskWithOutputDirs, [missingDir] as List)

        when:
        execute(task)

        then:
        task.outputDirs.get(0).isDirectory()
    }

    @Unroll
    def "validation fails for unspecified #property for #type.simpleName"() {
        given:
        def task = expectTaskCreated(type, [null] as Object[])

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "No value has been specified for property '$property'.")

        where:
        type                | property
        TaskWithInputFile   | 'inputFile'
        TaskWithOutputFile  | 'outputFile'
        TaskWithOutputFiles | 'outputFiles'
        TaskWithInputFiles  | 'input'
        TaskWithOutputDir   | 'outputDir'
        TaskWithOutputDirs  | 'outputDirs'
        TaskWithInputDir    | 'inputDir'
        TaskWithInput       | 'inputValue'
        TaskWithNestedBean  | 'bean.inputFile'
    }

    def validationActionFailsWhenSpecifiedOutputFileIsADirectory() {
        given:
        def task = expectTaskCreated(TaskWithOutputFile, existingDir)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to file '$task.outputFile' specified for property 'outputFile' as it is a directory.")
    }

    def validationActionFailsWhenSpecifiedOutputFilesIsADirectory() {
        given:
        def task = expectTaskCreated(TaskWithOutputFiles, [existingDir] as List)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to file '${task.outputFiles[0]}' specified for property 'outputFiles' as it is a directory.")
    }

    def validationActionFailsWhenSpecifiedOutputFileParentIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputFile, new File(testDir, "subdir/output.txt"))
        touch(task.outputFile.getParentFile())

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to file '$task.outputFile' specified for property 'outputFile', as ancestor '$task.outputFile.parentFile' is not a directory.")
    }

    def validationActionFailsWhenSpecifiedOutputFilesParentIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputFiles, [new File(testDir, "subdir/output.txt")] as List)
        touch(task.outputFiles.get(0).getParentFile())

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to file '${task.outputFiles[0]}' specified for property 'outputFiles', as ancestor '${task.outputFiles[0].parentFile}' is not a directory.")
    }

    def validationActionFailsWhenOutputDirectoryIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputDir, existingFile)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Directory '$task.outputDir' specified for property 'outputDir' is not a directory.")
    }

    def validationActionFailsWhenOutputDirectoriesIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputDirs, [existingFile] as List)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Directory '${task.outputDirs[0]}' specified for property 'outputDirs' is not a directory.")
    }

    def validationActionFailsWhenParentOfOutputDirectoryIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputDir, new File(testDir, "subdir/output"))
        touch(task.outputDir.getParentFile())

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to directory '$task.outputDir' specified for property 'outputDir', as ancestor '$task.outputDir.parentFile' is not a directory.")
    }

    def validationActionFailsWhenParentOfOutputDirectoriesIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithOutputDirs, [new File(testDir, "subdir/output")])
        touch(task.outputDirs.get(0).getParentFile())

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Cannot write to directory '${task.outputDirs[0]}' specified for property 'outputDirs', as ancestor '${task.outputDirs[0].parentFile}' is not a directory.")
    }

    def validationActionFailsWhenInputDirectoryDoesNotExist() {
        given:
        def task = expectTaskCreated(TaskWithInputDir, missingDir)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Directory '$task.inputDir' specified for property 'inputDir' does not exist.")
    }

    def validationActionFailsWhenInputDirectoryIsAFile() {
        given:
        def task = expectTaskCreated(TaskWithInputDir, existingFile)
        touch(task.inputDir)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "Directory '$task.inputDir' specified for property 'inputDir' is not a directory.")
    }

    def validatesNestedBeansWithPrivateType() {
        given:
        def task = expectTaskCreated(TaskWithNestedBeanWithPrivateClass, [existingFile, null] as Object[])

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "No value has been specified for property 'bean.inputFile'.")
    }

    def validationFailsWhenNestedBeanIsNull() {
        given:
        def task = expectTaskCreated(TaskWithNestedBean, [null] as Object[])
        task.clearBean()

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "No value has been specified for property 'bean'.")
    }

    def validationFailsWhenNestedBeanWithPrivateTypeIsNull() {
        given:
        def task = expectTaskCreated(TaskWithNestedBeanWithPrivateClass, [null, null] as Object[])
        task.clearBean()

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "No value has been specified for property 'bean'.")
    }

    def canAttachAnnotationToGroovyProperty() {
        given:
        def task = expectTaskCreated(InputFileTask)

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e, "No value has been specified for property 'srcFile'.")
    }

    def validationFailureListsViolationsForAllProperties() {
        given:
        def task = expectTaskCreated(TaskWithMultipleProperties, [null] as Object[])

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e,
            "No value has been specified for property 'outputFile'.",
            "No value has been specified for property 'bean.inputFile'.")
    }

    def propertyValidationJavaBeanSpecCase() {
        given:
        def task = expectTaskCreated(TaskWithJavaBeanCornerCaseProperties, [null, null, null, null, "a", "b"] as Object[])

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e,
            "No value has been specified for property 'cCompiler'.",
            "No value has been specified for property 'CFlags'.",
            "No value has been specified for property 'dns'.",
            "No value has been specified for property 'URL'.")
    }

    def propertyValidationJavaBeanSpecSingleChar() {
        given:
        def task = expectTaskCreated(TaskWithJavaBeanCornerCaseProperties, ["c", "C", "d", "U", null, null] as Object[])

        when:
        execute(task)

        then:
        def e = thrown WorkValidationException
        validateException(task, e,
            "No value has been specified for property 'a'.",
            "No value has been specified for property 'b'.")
    }

    @Unroll
    def "registers specified #target for #value on #type.simpleName"() {
        given:
        def task = expectTaskCreated(type, this[value])

        expect:
        task[target].files.files == [this[value]] as Set

        where:
        type               | target    | value
        TaskWithInputFile  | 'inputs'  | 'existingFile'
        TaskWithOutputFile | 'outputs' | 'existingFile'
        TaskWithOutputDir  | 'outputs' | 'missingDir'
    }

    def "registers specified list of inputs on TaskWithInputFiles"() {
        given:
        def values = ['testDir', 'missingFile'].collect({ this[it] })
        def task = expectTaskCreated(TaskWithInputFiles, values as List)

        expect:
        task.inputs.files.files == values as Set
    }

    @Unroll
    def "registers specified list of outputs for #value on #type.simpleName"() {
        given:
        def values = value.collect({ this[it] })
        def task = expectTaskCreated(type, values as List)

        expect:
        task.outputs.files.files == values as Set

        where:
        type                | value
        TaskWithOutputFiles | ['existingFile']
        TaskWithOutputDirs  | ['missingDir', 'missingDir2']
    }

    def registersSpecifiedInputDirectory() {
        given:
        def task = expectTaskCreated(TaskWithInputDir, existingDir)
        File file = existingDir.file("some-file").createFile()

        expect:
        task.inputs.files.files == [file] as Set
    }

    @Unroll
    def "registers input property for #prop on #type.simpleName"() {
        given:
        def task = (value == null) ? expectTaskCreated(type) : expectTaskCreated(type, value as Object[])

        expect:
        inputProperties(task)[prop] == expected

        where:
        type                                      | prop                 | value                            | expected
        TaskWithNestedBean                        | 'bean'               | [null]                           | Bean.class
        TaskWithNestedObject                      | 'bean.key'           | [['key': new Bean()]]            | Bean.class
        TaskWithNestedIterable                    | 'beans.$0'           | [new Bean()]                     | Bean.class
        TaskWithNestedIterable                    | 'beans.name$0.value' | [new NamedBean("name", "value")] | "value"
        TaskWithNestedBeanWithPrivateClass        | 'bean'               | [null, null]                     | Bean2.class
        TaskWithOptionalNestedBean                | 'bean'               | [null]                           | null
        TaskWithOptionalNestedBeanWithPrivateType | 'bean'               | null                             | null
        TaskWithInput                             | 'inputValue'         | ["value"]                        | "value"
        TaskWithBooleanInput                      | 'inputValue'         | [true]                           | true           // https://issues.gradle.org/Browse/GRADLE-2815
    }

    def "iterable nested properties are named by index"() {
        given:
        def task = expectTaskCreated(TaskWithNestedObject, [[new Bean(), new NamedBean('name', 'value'), new Bean()]] as Object[])

        expect:
        inputProperties(task).keySet() == ['bean.$0', 'bean.name$1', 'bean.name$1.value', 'bean.$2'] as Set
    }

    @Unroll
    def "registers properties #allTaskProperties on #type.simpleName"() {
        given:
        def task = (value == null) ? expectTaskCreated(type) : expectTaskCreated(type, value as Object[])

        when:
        def taskProperties = DefaultTaskProperties.resolve(propertyWalker, fileCollectionFactory, task)

        then:
        taskProperties.inputPropertyValues.get().keySet() == inputs as Set
        taskProperties.inputFileProperties*.propertyName as Set == inputFiles as Set
        taskProperties.outputFileProperties*.propertyName as Set == outputFiles as Set
        taskProperties.destroyableFiles.empty
        taskProperties.localStateFiles.empty

        where:
        type                                      | value                          | inputs                                          | inputFiles              | outputFiles
        TaskWithInput                             | ["value"]                      | ["inputValue"]                                  | []                      | []
        TaskWithBooleanInput                      | [true]                         | ["inputValue"]                                  | []                      | []
        TaskWithInputFile                         | [new File("some")]             | []                                              | ["inputFile"]           | []
        TaskWithInputFiles                        | [[new File("some")]]           | []                                              | ["input"]               | []
        BrokenTaskWithInputFiles                  | [[new File("some")]]           | []                                              | ["input"]               | []
        TaskWithInputDir                          | [new File("some")]             | []                                              | ["inputDir"]            | []
        BrokenTaskWithInputDir                    | [new File("some")]             | []                                              | ["inputDir"]            | []
        TaskWithOptionalInputFile                 | null                           | []                                              | ["inputFile"]           | []
        TaskWithOutputFile                        | [new File("some")]             | []                                              | []                      | ["outputFile"]
        TaskWithOutputFiles                       | [[new File("some")]]           | []                                              | []                      | ["outputFiles\$1"]
        TaskWithOutputDir                         | [new File("some")]             | []                                              | []                      | ["outputDir"]
        TaskWithOutputDirs                        | [[new File("some")]]           | []                                              | []                      | ["outputDirs\$1"]
        TaskWithOptionalOutputFile                | null                           | []                                              | []                      | []
        TaskWithOptionalOutputFiles               | null                           | []                                              | []                      | []
        TaskWithOptionalOutputDir                 | null                           | []                                              | []                      | []
        TaskWithOptionalOutputDirs                | null                           | []                                              | []                      | []
        TaskWithNestedBean                        | [null]                         | ["bean"]                                        | ["bean.inputFile"]      | []
        TaskWithNestedIterable                    | [new Bean()]                   | ["beans.\$0"]                                   | ["beans.\$0.inputFile"] | []
        TaskWithNestedBeanWithPrivateClass        | [null, null]                   | ["bean"]                                        | ["bean.inputFile"]      | []
        TaskWithOptionalNestedBean                | [null]                         | []                                              | []                      | []
        TaskWithOptionalNestedBean                | [new Bean()]                   | ["bean"]                                        | ["bean.inputFile"]      | []
        TaskWithOptionalNestedBeanWithPrivateType | null                           | []                                              | []                      | []
        TaskWithMultipleProperties                | [new File("some")]             | ["bean"]                                        | ["bean.inputFile"]      | ["outputFile"]
        TaskWithBridgeMethod                      | null                           | ["nestedProperty"]                              | []                      | ["nestedProperty.someOutputFile"]
        TaskWithJavaBeanCornerCaseProperties      | ["c", "C", "d", "U", "a", "b"] | ["cCompiler", "CFlags", "dns", "URL", "a", "b"] | []                      | []

        allTaskProperties = inputs + inputFiles + outputFiles
    }

    def "registers local state"() {
        def localState = new File(temporaryFolder.file('localState').createFile().absolutePath)
        given:
        def task = expectTaskCreated(TaskWithLocalState, localState)

        when:
        def taskProperties = DefaultTaskProperties.resolve(propertyWalker, fileCollectionFactory, task)

        then:
        taskProperties.localStateFiles.files as List == [localState]
        taskProperties.inputPropertyValues.get().isEmpty()
        taskProperties.inputFileProperties.empty
        taskProperties.outputFileProperties.empty
        taskProperties.destroyableFiles.empty
    }

    def "registers destroyables"() {
        def destroyable = new File(temporaryFolder.file('destroyable').createFile().absolutePath)
        given:
        def task = expectTaskCreated(TaskWithDestroyable, destroyable)

        when:
        def taskProperties = DefaultTaskProperties.resolve(propertyWalker, fileCollectionFactory, task)

        then:
        taskProperties.destroyableFiles.files as List == [destroyable]
        taskProperties.inputPropertyValues.get().isEmpty()
        taskProperties.inputFileProperties.empty
        taskProperties.outputFileProperties.empty
        taskProperties.localStateFiles.empty
    }

    @Unroll
    def "does not register #target for #type when not specified"() {
        given:
        def task = expectTaskCreated(type, value)

        expect:
        task[target].files.files.isEmpty()

        where:
        type                | target    | value
        TaskWithInputFile   | 'inputs'  | [null] as Object[]
        TaskWithOutputFile  | 'outputs' | [null] as Object[]
        TaskWithOutputFiles | 'outputs' | [null] as Object[]
        TaskWithOutputFiles | 'outputs' | [] as List
        TaskWithInputFiles  | 'inputs'  | [null] as Object[]
        TaskWithOutputDir   | 'outputs' | [null] as Object[]
        TaskWithOutputDirs  | 'outputs' | [null] as Object[]
        TaskWithOutputDirs  | 'outputs' | [] as List
        TaskWithInputDir    | 'inputs'  | [null] as Object[]
    }

    def skipsTaskWhenInputDirectoryIsEmptyAndSkipWhenEmpty() {
        given:
        def task = expectTaskCreated(BrokenTaskWithInputDir, existingDir)

        expect:
        execute(task)
    }

    def skipsTaskWhenInputFileCollectionIsEmpty() {
        given:
        def inputFiles = new ArrayList<File>()
        BrokenTaskWithInputFiles task = expectTaskCreated(BrokenTaskWithInputFiles, inputFiles)

        expect:
        execute(task)
    }

    @Unroll
    def "#description are not custom actions"() {
        given:
        def task = expectTaskCreated(type, [null] as Object[])

        expect:
        !task.hasCustomActions

        where:
        description                                         | type
        "task actions registered by processing annotations" | TestTask
        "validation actions"                                | TaskWithInputFile
        "directory creation actions"                        | TaskWithOutputDir
        "file creation actions"                             | TaskWithOutputFile
    }

    def ignoresBridgeMethods() {
        given:
        def task = expectTaskCreated(TaskWithBridgeMethod)

        when:
        task.outputs.files.files

        then:
        task.traversedOutputsCount == 1
    }

    def propertyExtractionJavaBeanSpec() {
        given:
        def task = expectTaskCreated(TaskWithJavaBeanCornerCaseProperties, "c", "C", "d", "U", "a", "b")
        def properties = inputProperties(task)

        expect:
        properties["cCompiler"] != null
        properties["CFlags"] != null
        properties["dns"] != null
        properties["URL"] != null
        properties["a"] != null
        properties["b"] != null
    }

    private <T extends TaskInternal> T expectTaskCreated(final Class<T> type, final Object... params) {
        final String name = "task"
        T task = AbstractTask.injectIntoNewInstance(project, TaskIdentity.create(name, type, project), new Callable<T>() {
            T call() throws Exception {
                if (params.length > 0) {
                    return type.cast(type.constructors[0].newInstance(params))
                } else {
                    return JavaReflectionUtil.newInstance(type)
                }
            }
        })
        return expectTaskCreated(name, type, task)
    }

    private <T extends TaskInternal> T expectTaskCreated(String name, final Class<T> type, T task) {
        // We cannot just stub here as we want to return a different task each time.
        def id = new TaskIdentity(type, name, null, null, null, 12)
        1 * delegate.create(id) >> task
        def createdTask = factory.create(id)
        assert createdTask.is(task)
        return task
    }

    private static void validateException(TaskInternal task, WorkValidationException exception, String... causes) {
        def expectedMessage = causes.length > 1 ? "Some problems were found with the configuration of $task" : "A problem was found with the configuration of $task"
        assert exception.message.contains(expectedMessage)
        assert exception.causes.collect({ it.message }) as Set == causes as Set
    }

    private Map<String, Object> inputProperties(TaskInternal task) {
        TaskPropertyTestUtils.getProperties(task, propertyWalker)
    }
}
